﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Editor.Common;
using ExcelDataReader;
using GamePlayConfig;
using UnityEditor;
using UnityEngine;

namespace Editor.Tools
{
    public static class Excel2Config
    {
        public const string EXCELS_DIR = "GamePlayConfig/Excels";
        public const string BIN_OUTPUT_DIR = "GamePlayConfig/Output";
        public const string CS_OUTPUT_DIR = "Scripts/GamePlayConfig";
        
        /**
         * 1.指定Excels目录
         * 2.对每个Excel文件执行以下操作
         *  a.生成一个配置.bin文件
         *  b.生成一个cs文件
         */
        private static void Generate(bool generateBin, bool generateCS)
        {
            var excels = GetExcelFiles(Path.Combine(Application.dataPath, EXCELS_DIR));
            foreach (var excel in excels)
            {
                using var stream = File.Open(excel, FileMode.Open, FileAccess.Read);
                try
                {
                    GenerateSingleExcel(stream, generateBin, generateCS);
                }
                catch (Exception e)
                {
                    Debug.LogError($"{excel} generate failed: {e.Message}");
                    Debug.LogError(e.StackTrace);
                }
            }

        }
        
        [MenuItem("Tools/GamePlayConfig/GenerateConfigCS_Bin")]
        public static void TestGenerate()
        {
            Generate(true, true);
        }
        
        [MenuItem("Tools/GamePlayConfig/GenerateConfigBin")]
        public static void TestGenerateBin()
        {
            Generate(true, false);
        }
        
        [MenuItem("Tools/GamePlayConfig/GenerateConfigCS")]
        public static void TestGenerateCS()
        {
            Generate(false, true);
        }
        

        public static void GenerateSingleExcel(Stream stream, bool generateBin, bool generateCS)
        {
            using var reader = ExcelReaderFactory.CreateReader(stream);
            do
            {
                reader.Read(); //Skip the first row 备注行
                
                // 导出开关行
                byte[] exportFlags = null;
                if (reader.Read())
                {
                    exportFlags = new byte[reader.FieldCount];
                    for (int column = 1; column < reader.FieldCount; ++column)
                    {
                        exportFlags[column] = Convert.ToByte(reader.GetDouble(column));
                    }
                }
                if (exportFlags == null)
                {
                    Debug.Log($"{reader.Name} Export flags is null");
                    continue;
                }
                
                // 类型行
                string[] fieldTypes = null;
                if (reader.Read())
                {
                    fieldTypes = new string[reader.FieldCount];
                    for (int column = 1; column < reader.FieldCount; ++column)
                    {
                        fieldTypes[column] = reader.GetString(column);
                    }
                }
                if (fieldTypes == null)
                {
                    Debug.Log($"{reader.Name} Types is null");
                    continue;
                }
                
                // 字段名行
                int idColumn = -1;
                if (reader.Read())
                {
                    for (int column = 1; column < reader.FieldCount; ++column)
                    {
                        if ("ID".Equals(reader.GetString(column)))
                        {
                            idColumn = column;
                            break;
                        }
                    }
                }
                if (idColumn == -1)
                {
                    Debug.Log($"{reader.Name} ID not defined in excel file");
                    continue;
                }
                

                if (generateCS)
                {
                    GenerateCS(reader, exportFlags, fieldTypes);
                }
                
                if (generateBin)
                {
                    GenerateBin(reader, exportFlags, fieldTypes, idColumn);
                }
            } while (reader.NextResult());
        }
        
        private static readonly Dictionary<string, Action<BinaryWriter, IExcelDataReader, int>> ExcelDataBinWriter = new Dictionary<string, Action<BinaryWriter, IExcelDataReader, int>>()
        {
            {"int", BinWriteInt},
            {"float", BinWriteFloat},
            {"string", BinWriteString},
            {"byte", BinWriteByte},
            {"int[]", BinWriteIntArray},
            {"float[]", BinWriteFloatArray},
            {"byte[]", BinWriteByteArray},
            {"string[]", BinWriteStringArray},
            {"int[,]", BinWriteInt2DArray},
            {"float[,]", BinWriteFloat2DArray},
            {"byte[,]", BinWriteByte2DArray},
            {"string[,]", BinWriteString2DArray}

        };
        
        private static void BinWriteInt(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            writer.Write(Convert.ToInt32(reader.GetDouble(column)));
        }
        
        private static void BinWriteFloat(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            writer.Write(Convert.ToSingle(reader.GetDouble(column)));
        }
        
        private static void BinWriteString(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            writer.Write(reader.GetString(column));
        }
        
        private static void BinWriteByte(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            writer.Write(Convert.ToByte(reader.GetDouble(column)));
        }

        // 1维数组
        private static void BinWriteArray<T>(BinaryWriter writer, IExcelDataReader reader, int column, Action<T> writeFunc) 
        {
            if (reader.IsDBNull(column))
            {
                writer.Write(0);
                return;
            }
            string content = reader.GetString(column);
            string[] splits = content.Split(",");
            writer.Write(splits.Length);

            foreach (var value in splits)
            {
                var convertedValue = Convert.ChangeType(value, typeof(T));
                writeFunc((T)convertedValue);
            }
        }
        
        private static void BinWriteIntArray(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            BinWriteArray<int>(writer, reader, column, writer.Write);
        }
        
        private static void BinWriteFloatArray(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            BinWriteArray<float>(writer, reader, column, writer.Write);
        }
        
        private static void BinWriteByteArray(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            BinWriteArray<byte>(writer, reader, column, writer.Write);
        }
        
        private static void BinWriteStringArray(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            BinWriteArray<string>(writer, reader, column, writer.Write);
        }
        
        // 2维数组
        private static void BinWrite2DArray<T>(BinaryWriter writer, IExcelDataReader reader, int column, Action<T> writeFunc) 
        {
            if (reader.IsDBNull(column))
            {
                writer.Write(0);
                writer.Write(0);
                return;
            }
            string content = reader.GetString(column);
            string[] splits = content.Split("|");

            writer.Write(splits.Length);
            writer.Write(splits.Length == 0 ? 0 : splits[0].Split(",").Length);
            
            foreach (var array in splits)
            {
                string[] values = array.Split(",");
                foreach (var value in values)
                {
                    var convertedValue = Convert.ChangeType(value, typeof(T));
                    writeFunc((T)convertedValue);
                }
            }
        }
        
        private static void BinWriteInt2DArray(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            BinWrite2DArray<int>(writer, reader, column, writer.Write);
        }
        
        private static void BinWriteFloat2DArray(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            BinWrite2DArray<float>(writer, reader, column, writer.Write);
        }
        
        private static void BinWriteByte2DArray(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            BinWrite2DArray<byte>(writer, reader, column, writer.Write);
        }
        
        private static void BinWriteString2DArray(BinaryWriter writer, IExcelDataReader reader, int column)
        {
            BinWrite2DArray<string>(writer, reader, column, writer.Write);
        }
        
        

        private static void GenerateBin(IExcelDataReader reader, IReadOnlyList<byte> exportFlags, IReadOnlyList<string> fieldTypes, int idColumn)
        {
            string fileFolderPath = Path.Combine(Application.dataPath, BIN_OUTPUT_DIR);
            if (!Directory.Exists(fileFolderPath))
            {
                Directory.CreateDirectory(fileFolderPath);
            }
            var outputFilePath = Path.Combine(fileFolderPath, reader.Name + ".bin");
            var hdFilePath = Path.Combine(fileFolderPath, reader.Name + $"{ConfigUtility.HEADER_SUFFIX}.bin");
            if (File.Exists(outputFilePath))
            {
                File.Delete(outputFilePath);
            }
            if (File.Exists(hdFilePath))
            {
                File.Delete(hdFilePath);
            }
            using var writer = new BinaryWriter(File.Open(outputFilePath, FileMode.Create));
            using var positionWriter = new BinaryWriter(File.Open(hdFilePath, FileMode.Create));
            bool isReachEnd = false;
            while (reader.Read()) //Each ROW
            {
                if (isReachEnd)
                {
                    break;
                }
                long position = writer.BaseStream.Position;

                int id = -1;
                for (int column = 0; column < reader.FieldCount; column++)
                {
                    if (column > exportFlags.Count)
                    {
                        Debug.Log($"{reader.Name} Export flags length is not match with column count");
                        break;
                    }

                    // 将ID和位置写入到hd文件
                    if (column == idColumn)
                    {
                        id = Convert.ToInt32(reader.GetDouble(column));
                        positionWriter.Write(id);
                        positionWriter.Write(position);
                    }

                    if (column == 0)
                    {
                        var _type = reader.GetFieldType(column);
                        if (_type == typeof(string))
                        {
                            var str = reader.GetString(column);
                            if (str.IndexOf("NO", StringComparison.Ordinal) != -1)
                            {
                                break;
                            }
                            if (str.IndexOf("END", StringComparison.Ordinal) != -1)
                            {
                                isReachEnd = true;
                            }
                        }
                        continue;
                    }
                    if ( exportFlags[column] == 0)
                    {
                        continue;
                    }

                    var type = fieldTypes[column];
                    var binWriter = ExcelDataBinWriter[type];
                    Debug.Assert(binWriter != null, $"No bin writer for type {type}");
                    binWriter?.Invoke(writer, reader, column);
                } // end for
            }// end while reader.Read
            Debug.Log($"Generate {reader.Name} bin file successfully at {Path.Combine(BIN_OUTPUT_DIR, reader.Name + ".bin")}" );
        }
        
        private static readonly Dictionary<string, string> TypeReader = new Dictionary<string, string>()
        {
            {"int", "ReadInt32()"},
            {"float", "ReadSingle()"},
            {"string", "ReadString()"},
            {"byte", "ReadByte()"},
        };
        
        private static void GenerateCS(IExcelDataReader reader, IReadOnlyList<byte> exportFlags, IReadOnlyList<string> fieldTypes)
        {
            var classInfo = new ClassInfo()
            {
                Path = CS_OUTPUT_DIR,
                ClassName = reader.Name + ConfigUtility.CFG_SUFFIX,
                Namespace = "GamePlayConfig",
                Fields = null, 
                Modifier = new []{ClassInfo.Public},
                Functions = Array.Empty<FunctionInfo>(),
                FileName = reader.Name + $"{ConfigUtility.CFG_SUFFIX}.cs",
                IsStruct = false,
                ParentClass = new []{$"Config<{reader.Name + ConfigUtility.CFG_SUFFIX}>", nameof(IBinaryDeserializable)},
                Using = new []{ "System.IO" }
            };

            // deserialize function
            const string binaryReader = "reader";
            var deserializeFunc = new FunctionInfo()
            {
                FunctionName = nameof(IBinaryDeserializable.Deserialize),
                ReturnType = "void",
                Modifier = new[] {ClassInfo.Public},
                Parameters = new[]
                {
                    new ParameterInfo(nameof(BinaryReader), binaryReader)
                },
            };
            
            var fields = new List<FieldInfo>();
            var deserializeFuncBody = new StringBuilder();
            bool IDDefined = false;
            for (int column = 1; column < reader.FieldCount; ++column)
            {
                // field define
                var fieldName = reader.GetString(column);
                
                if (exportFlags[column] == 0)
                    continue;

                var fieldType = fieldTypes[column];
                if (fieldName == "ID")
                {
                    IDDefined = true;
                }
                else
                {
                    fields.Add( new FieldInfo()
                    {
                        Type = fieldType,
                        FieldName = fieldName,
                        Modifier = new[] { ClassInfo.Public },
                        getter = "get",
                        setter = "private set"
                    });
                }
                
                // deserialize func body
                if (fieldType.IndexOf(",", StringComparison.Ordinal) != -1) // 2D array
                {
                    string baseType = fieldType.Substring(0, fieldType.IndexOf('['));
                    deserializeFuncBody.AppendLine($"            int len1_{fieldName} = {binaryReader}.ReadInt32();");
                    deserializeFuncBody.AppendLine($"            int len2_{fieldName} = {binaryReader}.ReadInt32();");
                    deserializeFuncBody.AppendLine($"            {fieldName} = new {baseType}[len1_{fieldName},len2_{fieldName}];");
                    deserializeFuncBody.AppendLine($"            for (int i = 0; i < len1_{fieldName}; ++i)");
                    deserializeFuncBody.AppendLine("            {");
                    deserializeFuncBody.AppendLine($"                for (int j = 0; j < len2_{fieldName}; ++j)");
                    deserializeFuncBody.AppendLine("                {");
                    deserializeFuncBody.AppendLine($"                    {fieldName}[i,j] = {binaryReader}.{TypeReader[baseType]};");
                    deserializeFuncBody.AppendLine("                }");
                    deserializeFuncBody.AppendLine("            }");
                    
                }
                else if (fieldType.IndexOf('[') != -1) // 1D array
                {
                    string baseType = fieldType.Substring(0, fieldType.IndexOf('['));
                    deserializeFuncBody.AppendLine($"            int len_{fieldName} = {binaryReader}.ReadInt32();");
                    deserializeFuncBody.AppendLine($"            {fieldName} = new {baseType}[len_{fieldName}];");
                    deserializeFuncBody.AppendLine($"            for (int i = 0; i < len_{fieldName}; ++i)");
                    deserializeFuncBody.AppendLine("            {");
                    deserializeFuncBody.AppendLine($"                {fieldName}[i] = {binaryReader}.{TypeReader[baseType]};");
                    deserializeFuncBody.AppendLine("            }");
                }
                else // single value
                {
                    deserializeFuncBody.AppendLine($"            {fieldName} = {binaryReader}.{TypeReader[fieldType]};");
                }
            }
            Debug.Assert(IDDefined, $"{reader.Name} ID not defined in excel file");
            
            classInfo.Fields = fields.ToArray();
            deserializeFunc.FunctionBody = deserializeFuncBody.ToString();
            classInfo.Functions = new []{deserializeFunc};
            ClassGenerator.Generate(classInfo);
            
        }
        
        public static IEnumerable<string> GetExcelFiles (string folderPath)
        {
            if (!Directory.Exists(folderPath)) return Array.Empty<string>();
            string[] allFiles = Directory.GetFiles(folderPath);
            return allFiles.Where(file => Path.GetExtension(file) == ".xlsm" || Path.GetExtension(file) == ".xls" || Path.GetExtension(file) == ".xlsx");
        }
    }
}